# -*- coding: utf-8 -*-

import errno
import socket


class Client:
	"""An agent that connects to an external host and provides an API to
	return data based on a protocol across that host."""

	connected = False
	conn      = None

	def __init__(self, addr, port, ssl_ctx=None, timeout=None, bind_addr=None):
		self.ssl_ctx = ssl_ctx
		self.addr = addr
		self.port = port

		if type(bind_addr) is tuple and len(bind_addr) == 2:
			pass
		elif type(bind_addr) is str:
			pass
		else:
			raise ValueError('`bind_addr`', type(bind_addr))

		ip = self._resolve(self.addr)
		self._setup_socket(ip, timeout, bind_addr)

	def _resolve(self, addr):
		from .resolver import resolve_dns_name
		return resolve_dns_name(addr)

	def _setup_socket(self, ip, timeout, bind_addr=None):
		from .core import _private_connect

		remote_addr = (ip, self.port)
		sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		sock.setblocking(0)

		if type(bind_addr) is tuple:
			sock.bind(bind_addr)
		elif type(bind_addr) is str:
			sock.bind((bind_addr, 0))

		try:
			sock.connect(remote_addr)
		except socket.error as exc:
			if exc.errno != errno.EINPROGRESS:
				raise
			_private_connect(self, ip, sock, self.addr, self.port, timeout=timeout)

	def __enter__(self):
		return self

	def __exit__(self, *args, **kw):
		self.close()

	def on_connect(self):
		pass

	def close(self):
		"""Close the socket to the remote host."""

		if not self.is_closed:
			self.conn.close()
			self.conn = None
			self.connected = True

	@property
	def is_closed(self):
		return not self.conn or self.conn.closed


class UDPClient(Client):
	def __init__(self, addr, port, source_ip=None):
		super().__init__(addr, port, source_ip=source_ip)

	def _setup_socket(self, ip, timeout, source_ip=None):
		from .core import UDPSocket

		sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
		sock.setblocking(0)

		if source_ip:
			sock.bind((source_ip, 0))

		self.conn = UDPSocket(self, sock, ip, self.port)
		self.connected = True

	def _resolve(self, addr):
		return addr

	class remote_addr:
		def __get__(self, inst, other):
			return (inst.addr, inst.port)

		def __set__(self, inst, value):
			inst.addr, inst.port = value

	remote_addr = remote_addr()
